]> vault307.fbx.one Git - Sensory_Wall.git/blob - circuitPython/audio_spectrum_lightshow/CircuitPython 7.x/code.py
more sensory wall projects
[Sensory_Wall.git] / circuitPython / audio_spectrum_lightshow / CircuitPython 7.x / code.py
1 # SPDX-FileCopyrightText: 2021 Phil Burgess for Adafruit Industries
2 #
3 # SPDX-License-Identifier: MIT
4
5 """
6 AUDIO SPECTRUM LIGHT SHOW for Adafruit EyeLights (LED Glasses + Driver).
7 Uses onboard microphone and a lot of math to react to music.
8 """
9
10 from array import array
11 from math import log
12 from time import monotonic
13 from supervisor import reload
14 import board
15 from audiobusio import PDMIn
16 from busio import I2C
17 import adafruit_is31fl3741
18 from adafruit_is31fl3741.adafruit_rgbmatrixqt import Adafruit_RGBMatrixQT
19 from rainbowio import colorwheel
20 from ulab import numpy as np
21 # if using CP7 and below:
22 from ulab.scipy.signal import spectrogram
23 # if using CP8 and above:
24 # from ulab.utils import spectrogram
25
26
27 # FFT/SPECTRUM CONFIG ----
28
29 fft_size = 256 # Sample size for Fourier transform, MUST be power of two
30 spectrum_size = fft_size // 2 # Output spectrum is 1/2 of FFT result
31 # Bottom of spectrum tends to be noisy, while top often exceeds musical
32 # range and is just harmonics, so clip both ends off:
33 low_bin = 10 # Lowest bin of spectrum that contributes to graph
34 high_bin = 75 # Highest bin "
35
36
37 # HARDWARE SETUP ---------
38
39 # Manually declare I2C (not board.I2C() directly) to access 1 MHz speed...
40 i2c = I2C(board.SCL, board.SDA, frequency=1000000)
41
42 # Initialize the IS31 LED driver, buffered for smoother animation
43 #glasses = LED_Glasses(i2c, allocate=adafruit_is31fl3741.MUST_BUFFER)
44 glasses = Adafruit_RGBMatrixQT(i2c, allocate=adafruit_is31fl3741.MUST_BUFFER)
45
46 glasses.show() # Clear any residue on startup
47 glasses.set_led_scaling(0xFF)
48 glasses.global_current = 5 # Not too bright please
49 glasses.enable = True
50
51 # Initialize mic and allocate recording buffer (default rate is 16 MHz)
52 mic = PDMIn(board.MICROPHONE_CLOCK, board.MICROPHONE_DATA, bit_depth=16)
53 rec_buf = array("H", [0] * fft_size) # 16-bit audio samples
54
55
56 # FFT/SPECTRUM SETUP -----
57
58 # To keep the display lively, tables are precomputed where each column of
59 # the matrix (of which there are few) is the sum value and weighting of
60 # several bins from the FFT spectrum output (of which there are many).
61 # The tables also help visually linearize the output so octaves are evenly
62 # spaced, as on a piano keyboard, whereas the source spectrum data is
63 # spaced by frequency in Hz.
64 column_table = []
65
66 spectrum_bits = log(spectrum_size, 2) # e.g. 7 for 128-bin spectrum
67 # Scale low_bin and high_bin to 0.0 to 1.0 equivalent range in spectrum
68 low_frac = log(low_bin, 2) / spectrum_bits
69 frac_range = log(high_bin, 2) / spectrum_bits - low_frac
70
71 for column in range(glasses.width):
72 # Determine the lower and upper frequency range for this column, as
73 # fractions within the scaled 0.0 to 1.0 spectrum range. 0.95 below
74 # creates slight frequency overlap between columns, looks nicer.
75 lower = low_frac + frac_range * (column / glasses.width * 0.95)
76 upper = low_frac + frac_range * ((column + 1) / glasses.width)
77 mid = (lower + upper) * 0.5 # Center of lower-to-upper range
78 half_width = (upper - lower) * 0.5 # 1/2 of lower-to-upper range
79 # Map fractions back to spectrum bin indices that contribute to column
80 first_bin = int(2 ** (spectrum_bits * lower) + 1e-4)
81 last_bin = int(2 ** (spectrum_bits * upper) + 1e-4)
82 bin_weights = [] # Each spectrum bin's weighting will be added here
83 for bin_index in range(first_bin, last_bin + 1):
84 # Find distance from column's overall center to individual bin's
85 # center, expressed as 0.0 (bin at center) to 1.0 (bin at limit of
86 # lower-to-upper range).
87 bin_center = log(bin_index + 0.5, 2) / spectrum_bits
88 dist = abs(bin_center - mid) / half_width
89 if dist < 1.0: # Filter out a few math stragglers at either end
90 # Bin weights have a cubic falloff curve within range:
91 dist = 1.0 - dist # Invert dist so 1.0 is at center
92 bin_weights.append(((3.0 - (dist * 2.0)) * dist) * dist)
93 # Scale bin weights so total is 1.0 for each column, but then mute
94 # lower columns slightly and boost higher columns. It graphs better.
95 total = sum(bin_weights)
96 bin_weights = [
97 (weight / total) * (0.8 + idx / glasses.width * 1.4)
98 for idx, weight in enumerate(bin_weights)
99 ]
100 # List w/five elements is stored for each column:
101 # 0: Index of the first spectrum bin that impacts this column.
102 # 1: A list of bin weights, starting from index above, length varies.
103 # 2: Color for drawing this column on the LED matrix. The 225 is on
104 # purpose, providing hues from red to purple, leaving out magenta.
105 # 3: Current height of the 'falling dot', updated each frame
106 # 4: Current velocity of the 'falling dot', updated each frame
107 column_table.append(
108 [
109 first_bin - low_bin,
110 bin_weights,
111 colorwheel(225 * column / glasses.width),
112 glasses.height,
113 0.0,
114 ]
115 )
116 # print(column_table)
117
118
119 # MAIN LOOP -------------
120
121 dynamic_level = 10 # For responding to changing volume levels
122 frames, start_time = 0, monotonic() # For frames-per-second calc
123
124 while True:
125 # The try/except here is because VERY INFREQUENTLY the I2C bus will
126 # encounter an error when accessing the LED driver, whether from bumping
127 # around the wires or sometimes an I2C device just gets wedged. To more
128 # robustly handle the latter, the code will restart if that happens.
129 try:
130 mic.record(rec_buf, fft_size) # Record batch of 16-bit samples
131 samples = np.array(rec_buf) # Convert to ndarray
132 # Compute spectrogram and trim results. Only the left half is
133 # normally needed (right half is mirrored), but we trim further as
134 # only the low_bin to high_bin elements are interesting to graph.
135 spectrum = spectrogram(samples)[low_bin : high_bin + 1]
136 # Linearize spectrum output. spectrogram() is always nonnegative,
137 # but add a tiny value to change any zeros to nonzero numbers
138 # (avoids rare 'inf' error)
139 spectrum = np.log(spectrum + 1e-7)
140 # Determine minimum & maximum across all spectrum bins, with limits
141 lower = max(np.min(spectrum), 4)
142 upper = min(max(np.max(spectrum), lower + 6), 20)
143
144 # Adjust dynamic level to current spectrum output, keeps the graph
145 # 'lively' as ambient volume changes. Sparkle but don't saturate.
146 if upper > dynamic_level:
147 # Got louder. Move level up quickly but allow initial "bump."
148 dynamic_level = upper * 0.7 + dynamic_level * 0.3
149 else:
150 # Got quieter. Ease level down, else too many bumps.
151 dynamic_level = dynamic_level * 0.5 + lower * 0.5
152
153 # Apply vertical scale to spectrum data. Results may exceed
154 # matrix height...that's OK, adds impact!
155 #data = (spectrum - lower) * (7 / (dynamic_level - lower))
156 data = (spectrum - lower) * ((glasses.height + 2) / (dynamic_level - lower))
157
158 for column, element in enumerate(column_table):
159 # Start BELOW matrix and accumulate bin weights UP, saves math
160 first_bin = element[0]
161 column_top = glasses.height + 1
162 for bin_offset, weight in enumerate(element[1]):
163 column_top -= data[first_bin + bin_offset] * weight
164
165 if column_top < element[3]: # Above current falling dot?
166 element[3] = column_top - 0.5 # Move dot up
167 element[4] = 0 # and clear out velocity
168 else:
169 element[3] += element[4] # Move dot down
170 element[4] += 0.2 # and accelerate
171
172 column_top = int(column_top) # Quantize to pixel space
173 for row in range(column_top): # Erase area above column
174 glasses.pixel(column, row, 0)
175 for row in range(column_top, glasses.height): # Draw column
176 glasses.pixel(column, row, element[2])
177 glasses.pixel(column, int(element[3]), 0xE08080) # Draw peak dot
178
179 glasses.show() # Buffered mode MUST use show() to refresh matrix
180
181 frames += 1
182 # print(frames / (monotonic() - start_time), "FPS")
183
184 except OSError: # See "try" notes above regarding rare I2C errors.
185 print("Restarting")
186 reload()